package org.ddevil.data;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.PriorityQueue;

import org.ddevil.data.ChangeTracker.ChangeTypes;
import org.ddevil.data.util.DataSetUtils;


/**
 * @author Eric Lundin
 */
public class KeyedChangeTracker implements Serializable {

	private static final long serialVersionUID = -3782780508825719733L;

	ArrayList<ArrayList<String>> keySet;

	ArrayList<KeyedChange> changeList;

	public KeyedChangeTracker(ArrayList<ArrayList<String>> keys) {
		keySet = keys;
		changeList = new ArrayList<KeyedChange>();
	}

	ArrayList<String> buildKeyListForLevel(int lvl) {
		ArrayList<String> keyList = new ArrayList<String>();
		for (int i = 0; i <= lvl; i++) {
			keyList.addAll(keySet.get(i));
		}
		return keyList;
	}

	public void clear() {
		changeList.clear();
	}

	public List<KeyedChange> getChanges() {
		Collections.sort(changeList, new TimeComparator());
		return changeList;
	}

	public void itemRemoved(Data oldValue, int level,
			boolean hadChilren) {
		updateForRemove(oldValue, level, hadChilren);
	}

	public void itemModified(Data oldValue, Data newValue,
			int level, boolean hadChildren) {
		updateForModify(oldValue, newValue, level, hadChildren);
	}

	public void itemAdded(Data atts, int level) {
		updateForAdd(atts, level);
	}

	/**
	 * 1. If an item has already been modified and is now being modified back
	 * remove the modify.<BR>
	 * 2. If the item has previously been modified update the modify to the new
	 * values.<BR>
	 * 3. If the modified item was previously added update the add to the new
	 * value.
	 */
	private void updateForModify(Data oldVal, Data newVal,
			int level, boolean hadChildren) {
		boolean addRec = true;
		UpdateHeap heap = createUpdateHeap(level);
		// 3. Update add records
		ArrayList<KeyedChange> addChanges = getChangesByNew(oldVal, level,
				ChangeTypes.ADD);
		for (int i = 0; i < addChanges.size(); i++) {
			KeyedChange currentChange = addChanges.get(i);
			if (currentChange.getLevel() == level) {
				currentChange.updateNewValAttributes(newVal,
						buildKeyListForLevel(level));
				addRec = false;
			} else {
				heap.queueUpdate(null, newVal, currentChange);
			}
		}

		// 1.Remove canceled modifies.
		// Item was a then was b now is a again.
		ArrayList<KeyedChange> matchOld = getChangesByOld(newVal, level,
				ChangeTypes.MODIFY);
		ArrayList<KeyedChange> matchNew = getChangesByNew(oldVal, level,
				ChangeTypes.MODIFY);
		ArrayList<KeyedChange> reverseMods = intersection(matchOld, matchNew);
		ArrayList<KeyedChange> rem = new ArrayList<KeyedChange>();
		if (reverseMods.size() > 0) {
			for (int i = 0; i < reverseMods.size(); i++) {
				KeyedChange currentChange = reverseMods.get(i);
				if (currentChange.getLevel() == level) {
					addRec = false;
					rem.add(currentChange);
				} else {
					heap.queueUpdate(newVal, oldVal, currentChange);
				}
			}
		}

		// Update modifies which haven't been broken yet.
		ArrayList<KeyedChange> updateMods = getChangesByNew(oldVal, level,
				ChangeTypes.MODIFY);
		updateMods.removeAll(reverseMods);
		if (updateMods.size() > 0) {
			for (KeyedChange currentChange : updateMods) {
				if (currentChange.getLevel() == level) {
					addRec = false;
					currentChange.updateNewValAttributes(newVal,
							buildKeyListForLevel(level));
				} else {
					heap.queueUpdate(newVal, newVal, currentChange);
				}
			}
		}
		if (addRec) {
			heap.queueModRec(oldVal, newVal, level, hadChildren);
		}
		heap.queueDeletion(rem);
		heap.update();
	}

	private ArrayList<KeyedChange> intersection(ArrayList<KeyedChange> a,
			ArrayList<KeyedChange> b) {
		ArrayList<KeyedChange> intersection = new ArrayList<KeyedChange>();
		KeyedChange currentChange;
		for (int i = 0; i < a.size(); i++) {
			currentChange = a.get(i);
			if (b.contains(currentChange)) {
				intersection.add(a.get(i));
			}
		}
		return intersection;
	}

	/**
	 * 1. If we remove a record that does not have an add record we must remove
	 * the modifications made to that record and create a removal record of the
	 * original value. <BR>
	 * 2. If we have an add for this record it should be up to date with any
	 * modify records. We need to remove this add record and any children.<BR>
	 * 3. Collapse any child removals into a parent removal. Set the hadchildren
	 * attribute to true.
	 */
	private void updateForRemove(Data oldVal, int level,
			boolean hadChildren) {
		UpdateHeap heap = createUpdateHeap(level);
		boolean addRecord = true;
		Data removeAtts = oldVal;
		// Get the modifications made to this thing.
		ArrayList<KeyedChange> modifications = getChangesByNew(oldVal, level,
				ChangeTypes.MODIFY);
		ArrayList<KeyedChange> rem = new ArrayList<KeyedChange>();

		// See if we have a modify. If the modify is on the same level as this
		// removal we should create a remove record. Since adds supposedly keep
		// track of their own mods. Get rid of the modify record regardless.
		for (KeyedChange currentChange : modifications) {
			if (currentChange.getLevel() == level) {
				removeAtts = currentChange.getOldValue();
			}
			rem.add(currentChange);
		}

		// Here we probably added a record, possibly modified it and then
		// decided we didn't like it. We could become more decisive or just
		// remove the add.
		// If we find an add with our level we need to remove the add.
		ArrayList<KeyedChange> defaultAdds = getChangesByNew(oldVal, level,
				ChangeTypes.ADD);
		for (KeyedChange current : defaultAdds) {
			if (current.getLevel() == level) {
				addRecord = false;
			}
			rem.add(current);
		}

		if (addRecord) {
			ArrayList<KeyedChange> collapseRemoves = getChangesByOld(
					removeAtts, level, ChangeTypes.REMOVE);
			for (KeyedChange change : collapseRemoves) {
				if (change.getLevel() != level) {
					hadChildren = true;
				}
				rem.add(change);
			}
			heap.queueRemRec(removeAtts, level, hadChildren);
		}
		heap.queueDeletion(rem);
		heap.update();

	}

	KeyedChange getChangeByIndx(int index) {
		return changeList.get(index);
	}

	private UpdateHeap createUpdateHeap(int level) {
		return new UpdateHeap(buildKeyListForLevel(level));
	}

	/**
	 * 1. If this add has previously been removed and the hadchildren attribute
	 * of the removed record is false remove the remove the record.<BR>
	 * 2. Otherwise add the record.
	 * 
	 * @param newVal
	 */
	private void updateForAdd(Data newVal, int level) {
		ArrayList<KeyedChange> removalsForCancel = getChangesByOld(newVal,
				level, ChangeTypes.REMOVE);
		boolean addRecord = true;
		UpdateHeap heap = createUpdateHeap(level);
		for (KeyedChange currentChange : removalsForCancel) {
			if (currentChange.hadChildren()) {
				removalsForCancel.remove(currentChange);
			}
		}

		if (addRecord) {
			heap.queueAddRec(newVal, level, false);
		}
		heap.queueDeletion(removalsForCancel);
		heap.update();
	}

	private ArrayList<KeyedChange> getChangesByOld(Data oldVal,
			int level, ChangeTypes type) {
		ArrayList<KeyedChange> changes = new ArrayList<KeyedChange>();
		for (int i = 0; i < changeList.size(); i++) {
			KeyedChange current = changeList.get(i);
			if (current.getType() == type
					&& equalToLevel(current.getOldValue(), oldVal, level)) {
				changes.add(current);
			}
		}
		return changes;
	}

	private ArrayList<KeyedChange> getChangesByNew(Data newVal,
			int level, ChangeTypes type) {
		ArrayList<KeyedChange> indices = new ArrayList<KeyedChange>();
		for (int i = 0; i < changeList.size(); i++) {
			KeyedChange current = changeList.get(i);
			if (current.getType() == type
					&& equalToLevel(current.getNewValue(), newVal, level)) {
				indices.add(current);
			}
		}
		return indices;
	}

	private boolean equalToLevel(Data val1, Data val2,
			int level) {
		return DataSetUtils.attribsEqual(val1, val2,
				buildKeyListForLevel(level));
	}

	public static class KeyedChange implements Serializable {
		private ChangeTypes type;

		private Data oldVal;

		private Data newVal;

		private int keyLevel;

		private long timeKey;

		/** This is the worst variable ever */
		private boolean hadChildren;

		public KeyedChange(ChangeTypes type, Data oldValue,
				Data newValue, int level, boolean nodeHadChildren) {
			setType(type);
			setOldValue(oldValue);
			setNewValue(newValue);
			keyLevel = level;
			hadChildren = nodeHadChildren;
			timeKey = System.nanoTime();
		}

		public KeyedChange(ChangeTypes type, Data oldValue,
				Data newValue, int level) {
			this(type, oldValue, newValue, level, false);
		}

		public boolean hadChildren() {
			return hadChildren;
		}

		public long getTimeKey() {
			return timeKey;
		}

		public int getLevel() {
			return keyLevel;
		}

		/**
		 * @return Returns the newVal.
		 */
		public Data getNewValue() {
			return newVal;
		}

		/**
		 * @param newVal
		 *            The newVal to set.
		 */
		private void setNewValue(Data newVal) {
			this.newVal = newVal;
		}

		/**
		 * @return Returns the oldVal.
		 */
		public Data getOldValue() {
			return oldVal;
		}

		/**
		 * @param oldVal
		 *            The oldVal to set.
		 */
		private void setOldValue(Data oldVal) {
			this.oldVal = oldVal;
		}

		/**
		 * @param type
		 *            The type to set.
		 */
		public void setType(ChangeTypes type) {
			this.type = type;
		}

		public ChangeTypes getType() {
			return this.type;
		}

		public void updateNewValAttributes(Data atts,
				ArrayList<String> ids) {
			for (String current : ids) {
				newVal.setDataItem(current, atts.getValue(current));
			}
			timeKey = System.nanoTime();
		}

		public void updateOldValAttributes(Data atts,
				ArrayList<String> ids) {
			for (String current : ids) {
				oldVal.setDataItem(current, atts.getValue(current));
			}
			timeKey = System.nanoTime();
		}

		@Override
		public String toString() {
			StringBuilder sb = new StringBuilder();
			sb.append(type);
			sb.append("\nOLD VALUE:\n" + oldVal);
			sb.append("\nNEW VALUE:\n" + newVal);
			return sb.toString();
		}
	}

	/**
	 * @author Eric Lundin
	 */
	protected class TimeComparator implements Comparator<KeyedChange> {
		/*
		 * (non-Javadoc)
		 * 
		 * @see java.util.Comparator#compare(java.lang.Object, java.lang.Object)
		 */
		public int compare(KeyedChange o1, KeyedChange o2) {
			int value = 0;
			if (o1.getTimeKey() < o2.getTimeKey()) {
				value = -1;
			} else if (o1.getTimeKey() > o2.getTimeKey()) {
				value = 1;
			}
			return value;
		}
	}

	protected abstract class QueuedUpdate implements Comparable<QueuedUpdate> {
		public abstract void performUpdate(ArrayList<String> keylist);

		public abstract int getRank();

		public int compareTo(QueuedUpdate qu) {
			return (int) Math.signum(getRank() - qu.getRank());
		}
	}

	private class QueuedModification extends QueuedUpdate {
		private Data updatedOldVal;

		private Data updatedNewVal;

		KeyedChange myChange;

		public QueuedModification(Data oldVal, Data newVal,
				KeyedChange change) {
			updatedOldVal = oldVal;
			updatedNewVal = newVal;
			myChange = change;
		}

		public int getLevel() {
			return myChange.getLevel();
		}

		public void performUpdate(ArrayList<String> keyList) {
			if (updatedOldVal != null) {
				myChange.updateOldValAttributes(updatedOldVal, keyList);
			}
			if (updatedNewVal != null) {
				myChange.updateNewValAttributes(updatedNewVal, keyList);
			}
		}

		public int getRank() {
			return getLevel();
		}
	}

	private class QueuedRecordInsert extends QueuedUpdate {
		Data oldVal;

		Data newVal;

		int myLevel;

		boolean hadChildren;

		ChangeTypes myType;

		public QueuedRecordInsert(Data oldValue,
				Data newValue, int level, boolean nodeHadChildren,
				ChangeTypes type) {
			oldVal = oldValue;
			newVal = newValue;
			myLevel = level;
			hadChildren = nodeHadChildren;
			myType = type;
		}

		/*
		 * (non-Javadoc)
		 * 
		 * @see org.ddevil.data.KeyedChangeTracker.QueuedUpdate#getRank()
		 */
		@Override
		public int getRank() {
			return myLevel;
		}

		/*
		 * (non-Javadoc)
		 * 
		 * @see org.ddevil.data.KeyedChangeTracker.QueuedUpdate#performUpdate(java.util.ArrayList)
		 */
		@Override
		public void performUpdate(ArrayList<String> keylist) {
			changeList.add(new KeyedChange(myType, oldVal, newVal, myLevel,
					hadChildren));
		}
	}

	private class QueuedDeletion extends QueuedUpdate {
		ArrayList<KeyedChange> myChanges;

		public QueuedDeletion(ArrayList<KeyedChange> changes) {
			myChanges = changes;
		}

		/*
		 * (non-Javadoc)
		 * 
		 * @see org.ddevil.data.KeyedChangeTracker.QueuedUpdate#getRank()
		 */
		@Override
		public int getRank() {
			// This needs to go last.
			return Integer.MAX_VALUE;
		}

		/*
		 * (non-Javadoc)
		 * 
		 * @see org.ddevil.data.KeyedChangeTracker.QueuedUpdate#performUpdate()
		 */
		@Override
		@SuppressWarnings("unused")
		public void performUpdate(ArrayList<String> keyList) {
			removeChanges(myChanges);
		}

		private void removeChanges(List<KeyedChange> changes) {
			if (changes != null) {
				changeList.removeAll(changes);
			}
		}
	}

	private class UpdateHeap extends PriorityQueue<QueuedUpdate> {

		ArrayList<String> keyList;

		public UpdateHeap(ArrayList<String> levelKeys) {
			keyList = levelKeys;
		}

		public void update() {
			QueuedUpdate current;
			while ((current = poll()) != null) {
				current.performUpdate(keyList);
			}
		}

		public void queueAddRec(Data newValue, int nodelevel,
				boolean hadChildren) {
			super.offer(new QueuedRecordInsert(null, newValue, nodelevel,
					hadChildren, ChangeTypes.ADD));
		}

		public void queueRemRec(Data oldValue, int nodelevel,
				boolean hadChildren) {
			super.offer(new QueuedRecordInsert(oldValue, null, nodelevel,
					hadChildren, ChangeTypes.REMOVE));
		}

		public void queueModRec(Data oldValue,
				Data newValue, int nodelevel, boolean hadChildren) {
			super.offer(new QueuedRecordInsert(oldValue, newValue, nodelevel,
					hadChildren, ChangeTypes.MODIFY));
		}

		public void queueUpdate(Data updatedOldValue,
				Data updatedNewValue, KeyedChange change) {
			super.offer(new QueuedModification(updatedOldValue,
					updatedNewValue, change));
		}

		public void queueDeletion(ArrayList<KeyedChange> deleteChanges) {
			super.offer(new QueuedDeletion(deleteChanges));
		}
	}

}
